package com.nulldev.util.internal.backport.httpclient_rw;

import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

import static java.lang.String.format;

import java.net.ProtocolException;
import java.nio.ByteBuffer;

import static com.nulldev.util.internal.backport.concurrency9.Objects.requireNonNull;
import static com.nulldev.util.internal.backport.httpclient_rw.impl.common.Utils.ACCEPT_ALL;

class Http1HeaderParser {

	private static final char CR = '\r';
	private static final char LF = '\n';
	private static final char HT = '\t';
	private static final char SP = ' ';

	private StringBuilder sb = new StringBuilder();
	private String statusLine;
	private int responseCode;
	private HttpHeaders headers;
	private Map<String, List<String>> privateMap = new ConcurrentHashMap<String, List<String>>();

	enum State {
		INITIAL,
		STATUS_LINE,
		STATUS_LINE_FOUND_CR,
		STATUS_LINE_FOUND_LF,
		STATUS_LINE_END,
		STATUS_LINE_END_CR,
		STATUS_LINE_END_LF,
		HEADER,
		HEADER_FOUND_CR,
		HEADER_FOUND_LF,
		HEADER_FOUND_CR_LF,
		HEADER_FOUND_CR_LF_CR,
		FINISHED
	}

	private State state = State.INITIAL;

	/** Returns the status-line. */
	String statusLine() {
		return statusLine;
	}

	/** Returns the response code. */
	int responseCode() {
		return responseCode;
	}

	/** Returns the headers, possibly empty. */
	HttpHeaders headers() {
		assert state == State.FINISHED : "Unexpected state " + state;
		return headers;
	}

	/**
	 * A current-state message suitable for inclusion in an exception detail
	 * message.
	 */
	public String currentStateMessage() {
		String stateName = state.name();
		String msg;
		if (stateName.contains("INITIAL")) {
			return format("HTTP/1.1 header parser received no bytes");
		} else if (stateName.contains("STATUS")) {
			msg = format("parsing HTTP/1.1 status line, receiving [%s]", sb.toString());
		} else if (stateName.contains("HEADER")) {
			String headerName = sb.toString();
			if (headerName.indexOf(':') != -1)
				headerName = headerName.substring(0, headerName.indexOf(':') + 1) + "...";
			msg = format("parsing HTTP/1.1 header, receiving [%s]", headerName);
		} else {
			msg = format("HTTP/1.1 parser receiving [%s]", state, sb.toString());
		}
		return format("%s, parser state [%s]", msg, state);
	}

	/**
	 * Parses HTTP/1.X status-line and headers from the given bytes. Must be called
	 * successive times, with additional data, until returns true.
	 *
	 * All given ByteBuffers will be consumed, until ( possibly ) the last one (
	 * when true is returned ), which may not be fully consumed.
	 *
	 * @param input the ( partial ) header data
	 * @return true iff the end of the headers block has been reached
	 */
	boolean parse(ByteBuffer input) throws ProtocolException {
		requireNonNull(input, "null input");

		while (canContinueParsing(input)) {
			switch (state) {
				case INITIAL:
					state = State.STATUS_LINE;
					break;
				case STATUS_LINE:
					readResumeStatusLine(input);
					break;
				// fallthrough
				case STATUS_LINE_FOUND_CR:
				case STATUS_LINE_FOUND_LF:
					readStatusLineFeed(input);
					break;
				case STATUS_LINE_END:
					maybeStartHeaders(input);
					break;
				// fallthrough
				case STATUS_LINE_END_CR:
				case STATUS_LINE_END_LF:
					maybeEndHeaders(input);
					break;
				case HEADER:
					readResumeHeader(input);
					break;
				// fallthrough
				case HEADER_FOUND_CR:
				case HEADER_FOUND_LF:
					resumeOrLF(input);
					break;
				case HEADER_FOUND_CR_LF:
					resumeOrSecondCR(input);
					break;
				case HEADER_FOUND_CR_LF_CR:
					resumeOrEndHeaders(input);
					break;
				default:
					throw new InternalError("Unexpected state: " + String.valueOf(state));
			}
		}

		return state == State.FINISHED;
	}

	private boolean canContinueParsing(ByteBuffer buffer) {
		// some states don't require any input to transition
		// to the next state.
		switch (state) {
			case FINISHED:
				return false;
			case STATUS_LINE_FOUND_LF:
				return true;
			case STATUS_LINE_END_LF:
				return true;
			case HEADER_FOUND_LF:
				return true;
			default:
				return buffer.hasRemaining();
		}
	}

	private void readResumeStatusLine(ByteBuffer input) {
		char c = 0;
		while (input.hasRemaining() && (c = (char) input.get()) != CR) {
			if (c == LF)
				break;
			sb.append(c);
		}
		if (c == CR) {
			state = State.STATUS_LINE_FOUND_CR;
		} else if (c == LF) {
			state = State.STATUS_LINE_FOUND_LF;
		}
	}

	private void readStatusLineFeed(ByteBuffer input) throws ProtocolException {
		char c = state == State.STATUS_LINE_FOUND_LF ? LF : (char) input.get();
		if (c != LF) {
			throw protocolException("Bad trailing char, \"%s\", when parsing status line, \"%s\"", c, sb.toString());
		}

		statusLine = sb.toString();
		sb = new StringBuilder();
		if (!statusLine.startsWith("HTTP/1.")) {
			throw protocolException("Invalid status line: \"%s\"", statusLine);
		}
		if (statusLine.length() < 12) {
			throw protocolException("Invalid status line: \"%s\"", statusLine);
		}
		responseCode = Integer.parseInt(statusLine.substring(9, 12));

		state = State.STATUS_LINE_END;
	}

	private void maybeStartHeaders(ByteBuffer input) {
		assert state == State.STATUS_LINE_END;
		assert sb.length() == 0;
		char c = (char) input.get();
		if (c == CR) {
			state = State.STATUS_LINE_END_CR;
		} else if (c == LF) {
			state = State.STATUS_LINE_END_LF;
		} else {
			sb.append(c);
			state = State.HEADER;
		}
	}

	private void maybeEndHeaders(ByteBuffer input) throws ProtocolException {
		assert state == State.STATUS_LINE_END_CR || state == State.STATUS_LINE_END_LF;
		assert sb.length() == 0;
		char c = state == State.STATUS_LINE_END_LF ? LF : (char) input.get();
		if (c == LF) {
			headers = HttpHeaders.of(privateMap, ACCEPT_ALL);
			privateMap = null;
			state = State.FINISHED; // no headers
		} else {
			throw protocolException("Unexpected \"%s\", after status line CR", c);
		}
	}

	private void readResumeHeader(ByteBuffer input) {
		assert state == State.HEADER;
		assert input.hasRemaining();
		while (input.hasRemaining()) {
			char c = (char) input.get();
			if (c == CR) {
				state = State.HEADER_FOUND_CR;
				break;
			} else if (c == LF) {
				state = State.HEADER_FOUND_LF;
				break;
			}

			if (c == HT)
				c = SP;
			sb.append(c);
		}
	}

	private void addHeaderFromString(String headerString) {
		assert sb.length() == 0;
		int idx = headerString.indexOf(':');
		if (idx == -1)
			return;
		String name = headerString.substring(0, idx).trim();
		if (name.isEmpty())
			return;
		String value = headerString.substring(idx + 1, headerString.length()).trim();

		privateMap.computeIfAbsent(name.toLowerCase(Locale.US), k -> new ArrayList<>()).add(value);
	}

	private void resumeOrLF(ByteBuffer input) {
		assert state == State.HEADER_FOUND_CR || state == State.HEADER_FOUND_LF;
		char c = state == State.HEADER_FOUND_LF ? LF : (char) input.get();
		if (c == LF) {
			// header value will be flushed by
			// resumeOrSecondCR if next line does not
			// begin by SP or HT
			state = State.HEADER_FOUND_CR_LF;
		} else if (c == SP || c == HT) {
			sb.append(SP); // parity with MessageHeaders
			state = State.HEADER;
		} else {
			sb = new StringBuilder();
			sb.append(c);
			state = State.HEADER;
		}
	}

	private void resumeOrSecondCR(ByteBuffer input) {
		assert state == State.HEADER_FOUND_CR_LF;
		char c = (char) input.get();
		if (c == CR || c == LF) {
			if (sb.length() > 0) {
				// no continuation line - flush
				// previous header value.
				String headerString = sb.toString();
				sb = new StringBuilder();
				addHeaderFromString(headerString);
			}
			if (c == CR) {
				state = State.HEADER_FOUND_CR_LF_CR;
			} else {
				state = State.FINISHED;
				headers = HttpHeaders.of(privateMap, ACCEPT_ALL);
				privateMap = null;
			}
		} else if (c == SP || c == HT) {
			assert sb.length() != 0;
			sb.append(SP); // continuation line
			state = State.HEADER;
		} else {
			if (sb.length() > 0) {
				// no continuation line - flush
				// previous header value.
				String headerString = sb.toString();
				sb = new StringBuilder();
				addHeaderFromString(headerString);
			}
			sb.append(c);
			state = State.HEADER;
		}
	}

	private void resumeOrEndHeaders(ByteBuffer input) throws ProtocolException {
		assert state == State.HEADER_FOUND_CR_LF_CR;
		char c = (char) input.get();
		if (c == LF) {
			state = State.FINISHED;
			headers = HttpHeaders.of(privateMap, ACCEPT_ALL);
			privateMap = null;
		} else {
			throw protocolException("Unexpected \"%s\", after CR LF CR", c);
		}
	}

	private ProtocolException protocolException(String format, Object... args) {
		return new ProtocolException(format(format, args));
	}
}